package org.digitarts.rtms.spring.groovy;

import groovy.lang.Binding;
import groovy.lang.GroovyCodeSource;
import groovy.lang.GroovyShell;
import groovy.lang.Script;

import java.io.IOException;
import java.net.URL;
import java.net.URLConnection;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;

import org.apache.log4j.Logger;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.digitarts.rtms.spring.collectors.formatter.IFormatter;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.BeanNameAware;

public class GroovyScriptManager implements BeanNameAware, BeanFactoryAware, IFormatter
{

	/** The groovy binding to pass context and state between the script and the renderer */
	protected Binding				binding					= null;
	/** The groovy shell to compile the script */
	protected GroovyShell			groovyShell				= null;
	/** The groovy script that executes the rendering and returns a byte array */
	protected Script				script					= null;
	/** The URL for the script source code if the code is supplied as a URL */
	protected URL					sourceUrl				= null;
	/** The groovy source code if code is supplied in-descriptor */
	protected String				sourceCode				= "String message = 'GroovyScriptManager Default Implementation'; println message; return message";
	/** The last sourceCode change time, used if code is supplied in-descriptor */
	protected long					sourceChangeTime		= 0L;
	/** The last time the script was compiled from source */
	protected long					lastCompileTime			= 0L;
	/** The groovy compilation properties */
	protected Properties			groovyProperties		= null;
	/** The compiler options */
	protected CompilerConfiguration	cc						= CompilerConfiguration.DEFAULT;
	/** The prepared groovy source */
	protected GroovyCodeSource		groovySourceCode		= null;
	/** The object logger */
	protected Logger				log						= null;
	/** The deployed bean name */
	protected String				beanName				= null;
	/** The deploying bean factory */
	protected BeanFactory			beanFactory				= null;
	/** a flag to indicate init status */
	protected boolean				inited					= false;
	/** state map */
	protected Map<String, Object>	state					= new HashMap<String, Object>();
	/** application properties */
	protected Properties			applicationProperties	= new Properties();

	public void setApplicationProperties(Properties applicationProperties)
	{
		this.applicationProperties.putAll(applicationProperties);
	}

	public void setState(Map<String, Object> state)
	{
		this.state.putAll(state);
	}

	public GroovyScriptManager()
	{
		log = Logger.getLogger(this.getClass());
		binding = new Binding();
	}

	public GroovyScriptManager(URL sourceUrl, Properties groovyProperties) throws IOException
	{
		this();
		this.sourceUrl = sourceUrl;
		this.groovyProperties = groovyProperties;
		init();
	}

	public void setBeanName(String beanName)
	{
		this.beanName = beanName;
	}

	public void setBeanFactory(BeanFactory beanFactory) throws BeansException
	{
		this.beanFactory = beanFactory;
	}

	public void init()
	{
		try
		{
			long start = System.currentTimeMillis();
			binding.setProperty("beanFactory", beanFactory);
			binding.setProperty("beanName", beanName);
			binding.setProperty("state", state);

			for (Entry<Object, Object> entry : applicationProperties.entrySet())
			{
				binding.setProperty(entry.getKey().toString().trim(), entry.getValue().toString().trim());
			}

			groovyShell = new GroovyShell(binding, cc);
			if (sourceUrl == null)
			{
				groovySourceCode = new GroovyCodeSource(sourceCode, beanName, "");
			}
			else
			{
				groovySourceCode = new GroovyCodeSource(sourceUrl);
			}
			groovySourceCode.setCachable(false);
			script = groovyShell.parse(groovySourceCode);
			lastCompileTime = System.currentTimeMillis();
			long elapsed = lastCompileTime - start;
			inited = true;
			if (sourceUrl != null)
			{
				log.info("Script [" + beanName + "] Compiled From [" + sourceUrl.toString() + "] In " + elapsed + " ms.");
			}
			else
			{
				log.info("Script [" + beanName + "] Compiled From In Line Source In " + elapsed + " ms.");
			}

		}
		catch (Exception e)
		{
			log.error("GroovyScriptManager Initialization Exception in [" + beanName + "]", e);
			throw new RuntimeException("GroovyScriptManager Initialization Exception in [" + beanName + "]", e);
		}
	}

	public Object execute()
	{
		checkForRecompile();
		return script.run();
	}

	public Object invokeMethod(String methodName, Object methodArguments)
	{
		checkForRecompile();
		return script.invokeMethod(methodName, methodArguments);
	}

	public Object run()
	{
		checkForRecompile();
		return script.run();
	}

	public String format(Object target)
	{
		checkForRecompile();
		script.getBinding().setProperty("formatTarget", target);
		return run().toString();
	}

	public void checkForRecompile()
	{
		if (!inited)
		{
			try
			{
				init();
			}
			catch (Exception e)
			{
				throw new RuntimeException("Failed to complete first time initialization", e);
			}
		}
		if (isModified())
		{
			try
			{
				long start = System.currentTimeMillis();
				groovySourceCode = new GroovyCodeSource(sourceUrl);
				groovySourceCode.setCachable(false);
				script = groovyShell.parse(groovySourceCode);
				lastCompileTime = System.currentTimeMillis();
				long elapsed = lastCompileTime - start;
				log.info("Script Compiled From [" + sourceUrl.toString() + "] In " + elapsed + " ms.");
			}
			catch (IOException ioe)
			{
				throw new RuntimeException("Failed to recompile script [" + sourceUrl + "]", ioe);
			}
		}
	}

	public boolean isModified()
	{
		if (sourceUrl == null)
		{
			return sourceChangeTime > lastCompileTime;
		}
		else
		{
			if (lastCompileTime == 0)
				return false;
			URLConnection urlConn = null;
			try
			{
				urlConn = sourceUrl.openConnection();
				long lastMod = urlConn.getLastModified();
				urlConn.getInputStream().close();
				return lastMod > lastCompileTime;
			}
			catch (Exception e)
			{
				return false;
			}
			finally
			{
				try
				{
					urlConn.getInputStream().close();
				}
				catch (Throwable t)
				{
				}
			}
		}
	}

	public URL getSourceUrl()
	{
		return sourceUrl;
	}

	public void setSourceUrl(URL sourceUrl)
	{
		this.sourceUrl = sourceUrl;
	}

	public Binding getBinding()
	{
		return binding;
	}

	public Properties getGroovyProperties()
	{
		return groovyProperties;
	}

	public GroovyShell getGroovyShell()
	{
		return groovyShell;
	}

	public long getLastCompileTime()
	{
		return lastCompileTime;
	}

	public Script getScript()
	{
		return script;
	}

	public void setCc(CompilerConfiguration cc)
	{
		this.cc = cc;
	}

	public String getSourceCode()
	{
		return sourceCode;
	}

	public void setSourceCode(String sourceCode)
	{
		this.sourceCode = sourceCode;
		sourceChangeTime = System.currentTimeMillis();
	}

}
